package ru.usu.gv.uicomponents.viewitems.edge;

import com.sun.j3d.utils.geometry.Cylinder;
import com.sun.j3d.utils.geometry.GeometryInfo;
import com.sun.j3d.utils.geometry.NormalGenerator;

import ru.usu.gv.core.GraphCanvas;
import ru.usu.gv.core.GraphEngine;
import ru.usu.gv.styles.StyleFacade;
import ru.usu.gv.uicomponents.UIElementBase;
import ru.usu.gv.uicomponents.UIUtilities;
import ru.usu.gv.uicomponents.UIVertex;
import ru.usu.gv.uicomponents.viewitems.ViewItemBase;
import ru.usu.gv.utils.GeometryUtil;

import javax.media.j3d.Appearance;
import javax.media.j3d.BranchGroup;
import javax.media.j3d.ColoringAttributes;
import javax.media.j3d.Geometry;
import javax.media.j3d.GeometryStripArray;
import javax.media.j3d.Shape3D;
import javax.media.j3d.TransformGroup;
import javax.vecmath.Color3f;
import javax.vecmath.Point2d;
import javax.vecmath.Point3d;
import javax.vecmath.Vector2d;

import java.awt.Color;
import java.awt.geom.Point2D;
import java.util.Arrays;

/**
 * @author spupyrev
 * 30.10.2009
 */
public class SplineTubeEdgeView extends ViewItemBase implements IEdgeViewItem
{
	private UIVertex v1;
	private UIVertex v2;

	private BranchGroup branchGroup;
	private Shape3D shape;
	private Point3d[] coordinates;
	private Point3d[] controlPoints;

	public SplineTubeEdgeView(Appearance appearance, TransformGroup transformGroup, UIVertex v1, UIVertex v2)
	{
		super(transformGroup, appearance, appearance.getMaterial());

		this.v1 = v1;
		this.v2 = v2;

		branchGroup = new BranchGroup();
		branchGroup.setCapability(BranchGroup.ALLOW_CHILDREN_READ);
		branchGroup.setCapability(BranchGroup.ALLOW_CHILDREN_WRITE);
		branchGroup.setCapability(BranchGroup.ALLOW_CHILDREN_EXTEND);

		shape = new Shape3D();
		shape.setAppearance(appearance);
		shape.setCapability(Shape3D.ALLOW_GEOMETRY_WRITE);
		shape.setCapability(Shape3D.ALLOW_BOUNDS_WRITE);
		shape.setBounds(GraphCanvas.getInstance().getBoundingSphere());

		controlPoints = new Point3d[] {};

		branchGroup.addChild(shape);
		transformGroup.addChild(branchGroup);
	}

	public void makePickable(UIElementBase ge)
	{
		ge.makePickable(shape);
	}

	@Override
	public void setColor(Color color)
	{
		super.setColor(color);
		Color3f color3f = new Color3f(color);
		getAppearance().getColoringAttributes().setColor(color3f);
		
		buildGeometry();
	}

	public Point3d[] getControlPoints()
	{
		return controlPoints;
	}

	public void rectificate()
	{
		setControlPoints(new Point3d[] {});
	}

	public void setControlPoints(Point3d[] controlPoints)
	{
		if (Arrays.equals(this.controlPoints, controlPoints))
			return;

		this.controlPoints = controlPoints;
		GraphEngine.getInstance().getUIGraphListeners().doChange(this);
	}

	public void moveToCanvasPosition(TransformGroup transformGroup, int x, int y)
	{
		Point2d hitPoint = new Point2d(x, y);
		Object[] res = UIUtilities.getDraggedControlPointIndex(transformGroup, coordinates, controlPoints, hitPoint);
		int controlPointDraggedIndex = (Integer) res[0];
		controlPoints = (Point3d[]) res[1];

		Point3d draggedControlPoint = controlPoints[controlPointDraggedIndex];
		controlPoints[controlPointDraggedIndex] = GeometryUtil.canvasToVWorldPosition(draggedControlPoint, hitPoint, transformGroup);

		GraphEngine.getInstance().getUIGraphListeners().doChange(this);
	}

	/** 
	 * New
	 */

	static Point3d[] tubePoints;
	static int[] tubeStripCounts;
	final static int xSize = 10, ySize = 10, segmentSize = 2 * (xSize + 1);
	final static double yStep = 1f / ySize;
	static NormalGenerator normalGenerator = new NormalGenerator();
	{
		Cylinder c = new Cylinder(0.5f, 1f, 0, xSize, ySize, new Appearance());
		c.getAppearance().setColoringAttributes(new ColoringAttributes());
		c.getAppearance().getColoringAttributes().setColor(new Color3f(1, 0, 0));
		GeometryStripArray tubeGeometry = getGeometry(c, Cylinder.BODY);
		tubePoints = new Point3d[tubeGeometry.getVertexCount()];
		tubeStripCounts = new int[tubeGeometry.getNumStrips()];
		loadGeometry(tubeGeometry, tubeStripCounts, tubePoints);
	}

	private static GeometryStripArray getGeometry(Cylinder c, int section)
	{
		return (GeometryStripArray) c.getShape(section).getGeometry();
	}

	private static void loadGeometry(GeometryStripArray geometry, int[] stripCounts, Point3d[] points)
	{
		for (int i = 0; i < points.length; i++)
		{
			points[i] = new Point3d();
		}
		geometry.getCoordinates(0, points);
		geometry.getStripVertexCounts(stripCounts);
	}

	private static Point2D.Double interpolate(Point2D.Double p0, Point2D.Double p1, double t)
	{
		return new Point2D.Double(t * p1.x + (1 - t) * p0.x, t * p1.y + (1 - t) * p0.y);
	}

	/**
	 * evaluates a bezier defined by the control polygon
	 * which points are given in the array at the value t
	 * returns the point on the curve (which is also returned in the first point in the
	 * input array, the line from that point to the second point in the array gives
	 * a tangent vector)
	 */
	private static Point2D.Double evalBezier(Point2D.Double[] arr, double t)
	{
		for (int iter = arr.length; iter > 0; iter--)
		{
			for (int i = 1; i < iter; i++)
			{
				arr[i - 1] = interpolate(arr[i - 1], arr[i], t);
			}
		}
		return arr[0];
	}

	public Geometry buildGeometry()
	{
		shape.removeAllGeometries();

		Point2D.Double[] t2pnts = new Point2D.Double[4];
		double radius = StyleFacade.getEdgeRadius();
		coordinates = buildCoordinates();
		int numCurves = coordinates.length / 4;
		double zLevel = 0.0;
		for (int n = 0; n < numCurves; n++)
		{
			Point3d[] taperedTubePoints = new Point3d[tubePoints.length];
			for (int i = 0; i < 4; i++)
				t2pnts[i] = new Point2D.Double(coordinates[n * 4 + i].x, coordinates[n * 4 + i].y);

			double y = 0;
			Vector2d v = new Vector2d(t2pnts[1].x - t2pnts[0].x, t2pnts[1].y - t2pnts[0].y);
			double cosTheta = -v.y / v.length();
			double sinTheta = Math.sin(Math.acos(cosTheta));
			if (v.x > 0)
			{
				sinTheta *= -1;
			}
			if (Double.isNaN(cosTheta))
			{
				cosTheta = 1.0;
				sinTheta = 0.0;
			}
			for (int segment = 0; segment < ySize; segment++)
			{
				// odd numbered points are in lower layer of segment,
				// even numbered points are in higher layer
				// want to visit lower layer, then higher layer
				for (int j = 1; true; j += 2)
				{
					if (j == segmentSize + 1)
					{
						j = 0;
						y += yStep;
						for (int i = 0; i < 4; i++)
							t2pnts[i] = new Point2D.Double(coordinates[n * 4 + i].x, coordinates[n * 4 + i].y);

						if (y < 0.9999)
						{
							evalBezier(t2pnts, y);
							v.set(t2pnts[1].x - t2pnts[0].x, t2pnts[1].y - t2pnts[0].y);
						}
						else
						{
							v.set(t2pnts[3].x - t2pnts[2].x, t2pnts[3].y - t2pnts[2].y);
							evalBezier(t2pnts, y);
						}
						cosTheta = -v.y / v.length();
						sinTheta = Math.sin(Math.acos(cosTheta));
						if (Double.isNaN(cosTheta))
						{
							cosTheta = 1.0;
							sinTheta = 0;
						}
						if (v.x > 0)
						{
							sinTheta *= -1;
						}
					}
					else if (j >= segmentSize)
					{
						break;
					}

					Point3d newPnt = new Point3d();
					Point3d oldPnt = tubePoints[segment * segmentSize + j];
					newPnt.x = -oldPnt.x * cosTheta * radius + t2pnts[0].x;
					newPnt.y = t2pnts[0].y + oldPnt.x * radius * sinTheta;
					newPnt.z = oldPnt.z * radius + zLevel;
					taperedTubePoints[segment * segmentSize + j] = newPnt;
				}
			}

			GeometryInfo gi = new GeometryInfo(GeometryInfo.TRIANGLE_STRIP_ARRAY);
			gi.setCoordinates(taperedTubePoints);
			Color3f[] colors = new Color3f[taperedTubePoints.length];
			for (int i = 0; i < taperedTubePoints.length; i++)
			{
				colors[i] = new Color3f();
				shape.getAppearance().getColoringAttributes().getColor(colors[i]);
			}
			gi.setColors(colors);
			// tube strip counts seem to be wrong in default geometry
			// so we recalculate again as follows (one strip for each tube segment):
			tubeStripCounts = new int[ySize];
			for (int j = 0; j < ySize; j++)
			{
				tubeStripCounts[j] = segmentSize;
			}
			gi.setStripCounts(tubeStripCounts);
			normalGenerator.generateNormals(gi);
			shape.addGeometry(gi.getGeometryArray());
			//shape.getAppearance().getColoringAttributes().setColor(new Color3f(1, 0, 0));
		}

		return null;
	}

	private Point3d[] buildCoordinates()
	{
		if (controlPoints.length == 0)
		{
			Point3d[] result = new Point3d[4];
			result[0] = new Point3d(v1.getPosition());
			result[1] = create(v1.getPosition(), 2.0 / 3.0, v2.getPosition(), 1.0 / 3.0);
			result[2] = create(v1.getPosition(), 1.0 / 3.0, v2.getPosition(), 2.0 / 3.0);
			result[3] = new Point3d(v2.getPosition());

			return result;
		}
		else
		{
			Point3d[] result = new Point3d[8 + controlPoints.length * 4];

			Point3d m = create(v1.getPosition(), controlPoints[0], 0.5);
			Point3d n;

			result[0] = new Point3d(v1.getPosition());
			result[1] = create(v1.getPosition(), 2.0 / 3.0, m, 1.0 / 3.0);
			result[2] = create(v1.getPosition(), 1.0 / 3.0, m, 2.0 / 3.0);
			result[3] = new Point3d(m);

			for (int i = 0; i < controlPoints.length; i++)
			{
				Point3d a = new Point3d(i > 0 ? controlPoints[i - 1] : v1.getPosition());
				Point3d b = new Point3d(controlPoints[i]);
				Point3d c = new Point3d(i + 1 < controlPoints.length ? controlPoints[i + 1] : v2.getPosition());
				m = create(a, b, 0.5);
				n = create(b, c, 0.5);

				result[4 + 4 * i] = new Point3d(m);
				result[4 + 4 * i + 1] = create(m, 1.0 / 3.0, b, 2.0 / 3.0);
				result[4 + 4 * i + 2] = create(n, 1.0 / 3.0, b, 2.0 / 3.0);
				result[4 + 4 * i + 3] = new Point3d(n);
			}

			n = create(controlPoints[controlPoints.length - 1], v2.getPosition(), 0.5);
			result[result.length - 4] = new Point3d(n);
			result[result.length - 3] = create(n, 2.0 / 3.0, v2.getPosition(), 1.0 / 3.0);
			result[result.length - 2] = create(n, 1.0 / 3.0, v2.getPosition(), 2.0 / 3.0);
			result[result.length - 1] = new Point3d(v2.getPosition());

			return result;
		}
	}

	private Point3d create(Point3d p0, Point3d p1, double scale)
	{
		return create(p0, scale, p1, scale);
	}

	private Point3d create(Point3d p0, double scale0, Point3d p1, double scale1)
	{
		p0 = new Point3d(p0);
		p0.scale(scale0);
		p1 = new Point3d(p1);
		p1.scale(scale1);

		Point3d res = new Point3d();
		res.add(p0, p1);
		return res;
	}

	public double getWidth()
	{
		throw new UnsupportedOperationException();
	}

	public void setWidth(double width)
	{
		throw new UnsupportedOperationException();
	}

}
